﻿using Padsurface.Audio.Engine;
using Padsurface.Audio.Engine.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Padsurface.Audio.Song.Model
{
    public class Song
    {
        private const double DefaultBeatsPerMinute = 90;
        private readonly IMetronome _metronome;
        private readonly IPlayBuffer _playBuffer;
        private readonly SortedSet<RecordedSound> _timeline;
        private readonly object _lock;
        private readonly double _loopBeatLength;
        private double _previousTickBeat;

        public event EventHandler<double> OnBeat;
        public event EventHandler SongStopped;

        public Guid Id { get; private set; }
        public string Name { get; set; }
        public List<Sound> Sounds { get; private set; }
        public TimeSpan CurrentTime { get; private set; }
        public bool IsLoopEnabled { get; set; }
        internal SortedSet<RecordedSound> Timeline { get { return _timeline; } }

        public bool IsRecording { get; private set; }
        public bool IsPlaying { get; private set; }

        public double CurrentBeat
        {
            get { return BeatsPerMinuteCalculator.GetBeat(CurrentTime.TotalSeconds, _beatsPerSecond); }
        }

        private double _beatsPerSecond;
        public double BeatsPerMinute 
        {
            get
            {
                return BeatsPerMinuteCalculator.GetBeatsPerMinute(_beatsPerSecond);
            }
            set
            {
                _beatsPerSecond = BeatsPerMinuteCalculator.GetBeatsPerSecond(value);
            }
        }

        public Song(IMetronome metronome, IPlayBuffer playBuffer)
        {
            if (metronome == null) throw new ArgumentNullException("metronome");
            if (playBuffer == null) throw new ArgumentNullException("playBuffer");

            _metronome = metronome;
            _metronome.OnTick += OnMetronomeTick;
            _playBuffer = playBuffer;
            
            _timeline = new SortedSet<RecordedSound>(new RecordedSoundComparer());
            _lock = new object();
            _loopBeatLength = BeatsPerMinuteCalculator.GetBeat(TimeSpan.FromSeconds(8), 120);

            Id = Guid.NewGuid();
            Sounds = new List<Sound>();
            IsLoopEnabled = true;
            BeatsPerMinute = DefaultBeatsPerMinute;
        }

        public Song(IMetronome metronome, IPlayBuffer playBuffer, Guid id)
            : this(metronome, playBuffer)
        {
            Id = id;

        }

        public async Task PlayFromStart()
        {
            lock (_lock)
            {
                CurrentTime = new TimeSpan();

                if (IsPlaying)
                    return;

                _playBuffer.Start();
                IsPlaying = true;
            }

            await _metronome.Start();
        }

        public async Task Stop()
        {
            lock (_lock)
            {
                if (!IsPlaying)
                    return;

                IsPlaying = false;
            }

            await _metronome.Stop();
            _playBuffer.Stop();
        }

        public async Task RecordFromStart()
        {
            lock (_lock)
            {
                CurrentTime = new TimeSpan();

                if (IsRecording)
                    return;

                IsRecording = true;
            }

            await PlayFromStart();
        }

        public void StopRecording()
        {
            lock (_lock)
            {
                if (!IsRecording)
                    return;

                IsRecording = false;
            }
        }

        public void Record(Sound sound)
        {
            if (sound == null) throw new ArgumentNullException("sound");

            if (!IsRecording)
                throw new InvalidOperationException("Start recording first, before recording a sound");

            lock (_lock)
            {
                _timeline.Add(new RecordedSound(CurrentBeat, sound));
            }
        }

        private void OnMetronomeTick(object sender, TimeSpan elapsedTime)
        {
            _previousTickBeat = CurrentBeat;
            CurrentTime = elapsedTime;

            if (IsPlaying)
                PlayCurrentTimeline();

            SetCurrentTimeOnSongEnd();

            if (OnBeat != null)
            {
                if (CurrentBeat % 1 < 0.01)
                    OnBeat(this, CurrentBeat);
            }
        }

        private void PlayCurrentTimeline()
        {
            IEnumerable<RecordedSound> soundsToPlay;
            lock (_lock)
            {
                soundsToPlay = _timeline.Where(s => s.Beat > _previousTickBeat && s.Beat <= CurrentBeat).ToList();
            }

            Parallel.ForEach(soundsToPlay, recordedSound =>
            {
                _playBuffer.Buffer.Add(recordedSound.Sound.WaveFileOutput);
            });
        }

        private void SetCurrentTimeOnSongEnd()
        {
            if (CurrentBeat < _loopBeatLength)
                return;

            if (IsLoopEnabled)
                ResetLoop();
            else
                StopLoop();
        }

        private void StopLoop()
        {
            StopRecording();
            var stopTask = Stop();

            if (SongStopped != null)
                SongStopped(this, EventArgs.Empty);
        }

        private void ResetLoop()
        {
            _metronome.Reset();
            CurrentTime = TimeSpan.Zero;
            _previousTickBeat = 0;
        }
    }
}